<?php
/**
 * Talents come from diligence, and knowledge is gained by accumulation.
 *
 * @author: 晋<657306123@qq.com>
 * @date: 2019/8/5 23:19
 */

namespace Xin\Filesystem\Adapter\QCloud;

use League\Flysystem\Config;
use Qcloud\Cos\Client;
use Qcloud\Cos\Exception\ServiceResponseException;
use Xin\Filesystem\Adapter\AbstractCloudAdapter;
use Xin\Filesystem\AdapterInterface;

/**
 * 腾讯云
 *
 * @link https://cloud.tencent.com/document/product/436/12266
 */
class QCloud extends AbstractCloudAdapter{

	/**
	 * @var \Qcloud\Cos\Client
	 */
	private $client;

	/**
	 * Qcloud constructor.
	 *
	 * @param array $config
	 * @throws \Xin\Filesystem\FilesystemException
	 */
	public function __construct(array $config){
		parent::__construct($config);

		$this->client = new Client([
			'region'      => isset($config['region']) ? $config['region'] : 'ap-beijing',
			'schema'      => isset($config['schema']) ? $config['schema'] : 'https', //协议头部，默认为http
			'credentials' => [
				'secretId'  => $config['ak'],
				'secretKey' => $config['sk'],
			],
		]);
	}

	/**
	 * @inheritDoc
	 */
	public function getPrivateFileUrl($path, $expires = 3600){
		$object = $this->applyPathPrefix($path);

		$options = [
			'Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http',
		];

		/** @var \Guzzle\Http\Url $objectUrl */
		$objectUrl = $this->client->getObjectUrl(
			$this->config['bucket'],
			$object,
			$expires,
			$options
		);

		return (string)$objectUrl;
	}

	/**
	 * @inheritDoc
	 */
	public function getUploadToken($key = null, $expires = 3600, $policy = null, $strictPolicy = true){
		// TODO: Implement getUploadToken() method.
	}

	/**
	 * @inheritDoc
	 */
	public function write($path, $contents, Config $config){
		$object = $this->applyPathPrefix($path);
		$options = $this->resolveOptions($config);

		try{
			$result = $this->client->Upload(
				$this->config['bucket'],
				$object,
				$contents,
				$options
			);
			$result = $this->normalizeFileInfo($result);
			$this->fileInfoPool->put($object, $result);

			return $result;
		}catch(ServiceResponseException $e){
			$this->lastError = $e->getMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function copy($path, $newpath){
		$object = $this->applyPathPrefix($path);
		$newobject = $this->applyPathPrefix($newpath);

		try{
			return $this->client->copyObject([
				'Bucket'     => $this->config['bucket'],
				'Key'        => $object,
				'CopySource' => $newobject,
				/*
				'MetadataDirective' => 'string',
				'ACL' => 'string',
				'CacheControl' => 'string',
				'ContentDisposition' => 'string',
				'ContentEncoding' => 'string',
				'ContentLanguage' => 'string',
				'ContentLength' => integer,
				'ContentType' => 'string',
				'Expires' => 'string',
				'GrantFullControl' => 'string',
				'GrantRead' => 'string',
				'GrantWrite' => 'string',
				'Metadata' => array(
				'string' => 'string',
				),
				'ContentMD5' => 'string',
				'ServerSideEncryption' => 'string',
				'StorageClass' => 'string'
				*/
			]);
		}catch(ServiceResponseException $e){
			$this->lastError = $e->getMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function delete($path){
		$object = $this->applyPathPrefix($path);

		try{
			return $this->client->deleteObject([
				'Bucket' => $this->config['bucket'],
				'Key'    => $object,
			]);
		}catch(ServiceResponseException $e){
			$this->lastError = $e->getMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function deleteDir($dirname){
		$object = $this->applyPathPrefix($dirname);

		try{
			return $this->client->deleteObject([
				'Bucket' => $this->config['bucket'],
				'Key'    => $object.'/',
			]);
		}catch(ServiceResponseException $e){
			$this->lastError = $e->getMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function createDir($dirname, Config $config){
		$object = $this->applyPathPrefix($dirname);

		try{
			return $this->client->putObject([
				'Bucket' => $this->config['bucket'],
				'Key'    => $object.'/',
				'Body'   => '',
			]);
		}catch(ServiceResponseException $e){
			$this->lastError = $e->getMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function setVisibility($path, $visibility){
		try{
			return $this->client->putObjectAcl([
				'Bucket' => $this->config['bucket'],
				'Key'    => $path,
				'ACL'    => $this->normalizeVisibility($visibility),
			]);
		}catch(ServiceResponseException $e){
			$this->lastError = $e->getMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function getVisibility($path){
		$object = $this->applyPathPrefix($path);

		try{
			$meta = $this->client->getObjectAcl([
				'Bucket' => $this->config['bucket'],
				'Key'    => $object,
			]);

			foreach($meta['Grants'] as $grant){
				if(isset($grant['Grantee']['URI'])
					&& $grant['Permission'] === 'READ'
					&& strpos($grant['Grantee']['URI'], 'global/AllUsers') !== false
				){
					return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
				}
			}

			return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
		}catch(ServiceResponseException $e){
			$this->lastError = $e->getMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function read($path){
		$object = $this->applyPathPrefix($path);

		try{
			$response = $this->forceReadFromCDN()
				? $this->readFromCDN($object)
				: $this->readFromSource($object);
			return ['contents' => (string)$response];
		}catch(ServiceResponseException $e){
			$this->lastError = $e->getMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	public function readStream($path){
		try{
			$temporaryUrl = $this->getPrivateFileUrl($path);
			$stream = $this->getHttpClient()
				->get($temporaryUrl, ['stream' => true])
				->getBody()
				->detach();
			return ['stream' => $stream];
		}catch(ServiceResponseException $e){
			return false;
		}
	}

	/**
	 * @return \Guzzle\Http\Client
	 */
	protected function getHttpClient(){
		return new \Guzzle\Http\Client([
			'timeout'         => $this->config['timeout'],
			'connect_timeout' => $this->config['connect_timeout'],
		]);
	}

	/**
	 * @return bool
	 */
	protected function forceReadFromCDN(){
		return $this->config['cdn']
			&& isset($this->config['read_from_cdn'])
			&& $this->config['read_from_cdn'];
	}

	/**
	 * @param string $path
	 * @return string
	 */
	protected function readFromSource($path){
		try{
			$response = $this->client->getObject([
				'Bucket' => $this->config['bucket'],
				'Key'    => $path,
			]);
			return $response['Body'];
		}catch(ServiceResponseException $e){
			return false;
		}
	}

	/**
	 * @param string $path
	 * @return string
	 */
	protected function readFromCDN($path){
		return $this->getHttpClient()
			->get($path)
			->getBody()
			->getContents();
	}

	/**
	 * @inheritDoc
	 */
	public function listContents($directory = '', $recursive = false){
		$directory = $this->applyPathPrefix($directory);

		$list = [];
		$marker = '';
		while(true){
			$response = $this->listObjects($directory, $recursive, $marker);

			foreach((array)$response['Contents'] as $content){
				$list[] = $this->normalizeFileInfo($content);
			}

			if(!$response['IsTruncated']){
				break;
			}
			$marker = $response['NextMarker'] ?: '';
		}

		return $list;
	}

	/**
	 * @param string $directory
	 * @param bool   $recursive
	 * @param string $marker max return 1000 record, if record greater than 1000
	 *                          you should set the next marker to get the full list
	 * @return array
	 */
	protected function listObjects($directory = '', $recursive = false, $marker = ''){
		try{
			return $this->client->listObjects([
				'Bucket'    => $this->config['bucket'],
				'Prefix'    => ((string)$directory === '') ? '' : ($directory.'/'),
				'Delimiter' => $recursive ? '' : '/',
				'Marker'    => $marker,
				'MaxKeys'   => 1000,
			]);
		}catch(ServiceResponseException $e){
			return [
				'Contents'    => [],
				'IsTruncated' => false,
				'NextMarker'  => '',
			];
		}
	}

	/**
	 * @inheritDoc
	 */
	public function getMetadata($path){
		$object = $this->applyPathPrefix($path);

		try{
			return $this->client->headObject([
				'Bucket' => $this->config['bucket'],
				'Key'    => $object,
			]);
		}catch(ServiceResponseException $e){
			$this->lastError = $e->getMessage();
			return false;
		}
	}

	/**
	 * @inheritDoc
	 */
	protected function normalizeFileInfo($stat){
		$path = pathinfo($stat['Key']);

		return [
			'type'      => substr($stat['Key'], -1) === '/' ? 'dir' : 'file',
			'path'      => $stat['Key'],
			'timestamp' => strtotime($stat['LastModified'])->getTimestamp(),
			'size'      => (int)$stat['Size'],
			'dirname'   => $path['dirname'] === '.' ? '' : (string)$path['dirname'],
			'basename'  => (string)$path['basename'],
			'extension' => isset($path['extension']) ? $path['extension'] : '',
			'filename'  => (string)$path['filename'],
		];
	}

	/**
	 * @param $visibility
	 * @return string
	 */
	protected function normalizeVisibility($visibility){
		switch($visibility){
			case AdapterInterface::VISIBILITY_PUBLIC:
				$visibility = 'public-read';
				break;
		}

		return $visibility;
	}
}
